package com.gambi.tapestry.services.gwt;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

import org.apache.tapestry5.internal.services.AssetResourceLocator;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.services.ContextPathEncoder;
import org.apache.tapestry5.services.HttpServletRequestFilter;
import org.apache.tapestry5.services.HttpServletRequestHandler;
import org.slf4j.Logger;

import com.gambi.tapestry.utils.GWTConstant;
import com.gambi.tapestry.utils.GWTUtils;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.RPCRequest;
import com.google.gwt.user.server.rpc.RPCServletUtils;

/*
 * TODO Re-think the whole parameter passing !!!! 
 */
public class GWTServletHttpRequestFilter implements HttpServletRequestFilter {

	private final Logger logger;
	private final AssetResourceLocator assetResourceLocator;
	private final ContextPathEncoder contextPathEncoder;

	private HttpServletRequest req;

	public GWTServletHttpRequestFilter(
			AssetResourceLocator assetResourceLocator,
			ContextPathEncoder contextPathEncoder, Logger logger) {
		this.assetResourceLocator = assetResourceLocator;
		this.contextPathEncoder = contextPathEncoder;
		this.logger = logger;
	}

	private boolean handleGwtEncryptedRequest(HttpServletRequest request,
			HttpServletResponse response, HttpServletRequestHandler handler) {
		try {
			/*
			 * Wrap the original request to export the GWT Rpc call parameters
			 * as RequestParameters
			 */
			HttpServletRequest newRequest = createGWTEncryptedWrappedRequest(request);
			System.out.println("GWTServletHttpRequestFilter.service() "
					+ newRequest.getServletPath());

			/*
			 * Wrap the original response to intercepts the Tapestry output and
			 * encode to GWT if it is necessary
			 */
			final GwtOutputStream gwtOutputStream = new GwtOutputStream(
					newRequest, response);

			HttpServletResponse newResponse = new HttpServletResponseWrapper(
					response) {
				@Override
				public ServletOutputStream getOutputStream() throws IOException {
					return gwtOutputStream;
				}
			};

			// Let the request flows inside the system
			return handler.service(newRequest, newResponse);
		} catch (ServletException e) {
			logger.error("servlet problem {}", e);
			throw new RuntimeException(e);

		} catch (IOException e) {
			logger.error("servlet problem {}", e);
			throw new RuntimeException(e);
		} catch (SerializationException e) {
			logger.error("servlet problem {}", e);
			throw new RuntimeException(e);
		}
	}

	private boolean handleGwtPlainRequest(HttpServletRequest request,
			HttpServletResponse response, HttpServletRequestHandler handler) {
		try {
			// Extract the JSON Objects to Parameters, Wrap the requests and
			// push it into the system
			BufferedReader in = new BufferedReader(  new InputStreamReader( request.getInputStream() ) );
			StringBuffer sb = new StringBuffer();
			String line = in.readLine();
			while( line != null ){
				sb.append( line );
				line = in.readLine();
			}
			
			System.out.println("GWTServletHttpRequestFilter.handleGwtPlainRequest() "  + sb.toString() );
			
			// Try to convert into a JSON Object
			JSONObject jsonObject = new JSONObject( sb.toString() );
			return handler.service(createGWTPlainWrappedRequest(request, jsonObject), response);
		} catch (IOException e) {
			logger.error("servlet problem {}", e);
			throw new RuntimeException(e);
		} catch (Throwable e) {
			logger.error("servlet problem {}", e);
			throw new RuntimeException(e);
		}
		
	}

	protected HttpServletRequest createGWTPlainWrappedRequest(
			final HttpServletRequest request, final JSONObject jsonObject) throws ServletException, IOException {

		HttpServletRequest newRequest = new HttpServletRequestWrapper(request) {
			@Override
			public String getHeader(String name) {
				HttpServletRequest httpRequest = (HttpServletRequest) getRequest();
				if (httpRequest.getHeader("X-Requested-With") != null) {
					return super.getHeader(name);
				}
				if ("X-Requested-With".equals(name)) {
					return "XMLHttpRequest";
				}

				return super.getHeader(name);
			}

			// Taken from:
			// http://vangjee.wordpress.com/2009/02/25/how-to-modify-request-headers-in-a-j2ee-web-application/
			@Override
			public Enumeration getHeaderNames() {
				List list = new ArrayList();

				// loop over request headers from wrapped request object
				HttpServletRequest request = (HttpServletRequest) getRequest();
				Enumeration e = request.getHeaderNames();
				while (e.hasMoreElements()) {
					// add the names of the request headers into the list
					String n = (String) e.nextElement();
					list.add(n);
				}

				if (list.contains("X-Requested-With")) {
					return e;
				}

				// additionally, add the "username" to the list of request
				// header names
				list.add("X-Requested-With");

				// create an enumeration from the list and return
				Enumeration en = Collections.enumeration(list);
				return en;
			}

			@Override
			public Map getParameterMap() {
				Map parametersMap = new HashMap(super.getParameterMap());
				// Add the Plain GWT Parameters: assumption simple String parameters
				for( String key : jsonObject.keys() ){
					// Elaborate the parameter
					parametersMap.put( key , ( jsonObject.isNull(key) ) ? "" : jsonObject.get(key) );
				}
				return parametersMap;
			}

			@Override
			public Enumeration getParameterNames() {
				Enumeration enumeration = Collections
						.enumeration(getParameterMap().keySet());
				return enumeration;
			}

			@Override
			public String[] getParameterValues(String name) {
				// We did not ass any String[] object to the map, thus forward
				// the call up
				return super.getParameterValues(name);
			}

			@Override
			public String getParameter(String name) {
				return (String) getParameterMap().get(name);
			}

		};
		return newRequest;
	}

	public boolean service(HttpServletRequest request,
			HttpServletResponse response, HttpServletRequestHandler handler)
			throws IOException {

		this.req = request;
		
		for( Object p : request.getParameterMap().keySet()){
			System.out.println( p + " -- " + request.getParameter( (String) p ) );
		}
		
		
		
		if (GWTUtils.isGwtEncryptedRequest(request)) {
			return handleGwtEncryptedRequest(request, response, handler);
		} else

		if (GWTUtils.isGwtPlainRequest(request)) {
			return handleGwtPlainRequest(request, response, handler);
		} else {

			return handler.service(request, response);
		}
	}

	RPCRequest decodeRPCRequest(HttpServletRequest request) throws IOException {
		try {
			String payload = RPCServletUtils.readContentAsGwtRpc(request);
			return RPC.decodeRequest(payload);
		} catch (ServletException e) {
			logger.error("servlet problem {}", e);
			throw new IOException(e.getMessage());
		}
	}

	protected HttpServletRequest createGWTEncryptedWrappedRequest(
			HttpServletRequest request) throws ServletException, IOException,
			SerializationException {

		final RPCRequest rpcRequest = decodeRPCRequest(request);

		HttpServletRequest newRequest = new HttpServletRequestWrapper(request) {

			/*
			 * By Convention: if the event name is not specified we use the
			 * method name as event name.
			 * 
			 * TODO: What if the servletURL is wrong ? Just return it...
			 */
			@Override
			public String getServletPath() {
				String servletPath = super.getServletPath();
				if (servletPath.endsWith(":")) {
					return servletPath + rpcRequest.getMethod().getName();
				} else if (servletPath.contains(":")
						|| !servletPath.contains(".")) {
					return servletPath;
				} else {
					// TODO Note that the action convention is not valid in this
					// context
					// /chooser.select/bla/bla -->
					// /chooser.select:methodName/bla/bla

					// /chooser.select/bla/bla --> select/bla/bla
					String componentID = servletPath.substring(
							servletPath.lastIndexOf(".") + 1,
							servletPath.length());
					if (componentID.contains("/")) {
						componentID = componentID.substring(0,
								componentID.indexOf("/"));
					}
					return servletPath.replaceFirst("." + componentID, "."
							+ componentID + ":"
							+ rpcRequest.getMethod().getName());
				}
			}

			// Force the Ajax header for Tapestry
			@Override
			public String getHeader(String name) {
				HttpServletRequest httpRequest = (HttpServletRequest) getRequest();
				if (httpRequest.getHeader("X-Requested-With") != null) {
					return super.getHeader(name);
				}
				if ("X-Requested-With".equals(name)) {
					return "XMLHttpRequest";
				}

				return super.getHeader(name);
			}

			// Taken from:
			// http://vangjee.wordpress.com/2009/02/25/how-to-modify-request-headers-in-a-j2ee-web-application/
			@Override
			public Enumeration getHeaderNames() {
				List list = new ArrayList();

				// loop over request headers from wrapped request object
				HttpServletRequest request = (HttpServletRequest) getRequest();
				Enumeration e = request.getHeaderNames();
				while (e.hasMoreElements()) {
					// add the names of the request headers into the list
					String n = (String) e.nextElement();
					list.add(n);
				}

				if (list.contains("X-Requested-With")) {
					return e;
				}

				// additionally, add the "username" to the list of request
				// header names
				list.add("X-Requested-With");

				// create an enumeration from the list and return
				Enumeration en = Collections.enumeration(list);
				return en;
			}

			@Override
			public Map getParameterMap() {
				Map parametersMap = new HashMap(super.getParameterMap());
				// Add the GWT Parameters

				// Mappa dei parametri va fatta cosi index=type
				// index e l'ordine della signature originale e la posizione
				// nell'array dei parametri
				// della rpc request per il getObject

				for (int i = 0; i < rpcRequest.getParameters().length; i++) {
					parametersMap
							.put((String) GWTConstant.GWT_REQUEST_PARAMETER_ABSOLUTE_PREFIX
									+ i, rpcRequest.getMethod()
									.getParameterTypes()[i].getName());
				}
				return parametersMap;
			}

			@Override
			public Enumeration getParameterNames() {

				Enumeration enumeration = Collections
						.enumeration(getParameterMap().keySet());
				return enumeration;
			}

			@Override
			public String[] getParameterValues(String name) {
				// We did not ass any String[] object to the map, thus forward
				// the call up
				return super.getParameterValues(name);
			}

			@Override
			public String getParameter(String name) {
				return (String) getParameterMap().get(name);
			}

		};

		// Add the Reflection class to request attributes - This is the only
		// thing we need to use to move around objects and data
		newRequest.setAttribute(RPCRequest.class.getName(), rpcRequest);

		return newRequest;
	}

	// protected String readContent(HttpServletRequest request)
	// throws ServletException, IOException {
	// return RPCServletUtils.readContentAsUtf8(request, true);
	// }

	// TODO I must rehearse the Details About serialization

	// protected boolean shouldCompressResponse(HttpServletRequest request,
	// HttpServletResponse response, String responsePayload) {
	// return RPCServletUtils
	// .exceedsUncompressedContentLengthLimit(responsePayload);
	// }
	//
	// public final SerializationPolicy getSerializationPolicy(
	// String moduleBaseURL, String strongName) {
	// SerializationPolicy serializationPolicy = null;
	//
	// serializationPolicy = loadSerializationPolicy(req, moduleBaseURL,
	// strongName);
	//
	// if (serializationPolicy == null) {
	// // Failed to get the requested serialization policy; use the default
	// logger.info("WARNING: Failed to get the SerializationPolicy '"
	// + strongName
	// + "' for module '"
	// + moduleBaseURL
	// +
	// "'; a legacy, 1.3.3 compatible, serialization policy will be used.  You may experience SerializationExceptions as a result.");
	// serializationPolicy = RPC.getDefaultSerializationPolicy();
	// }
	// return serializationPolicy;
	// }
	//
	// SerializationPolicy loadSerializationPolicy(HttpServletRequest request,
	// String moduleBaseURL, String strongName) {
	// // The request can tell you the path of the web app relative to the
	// // container root.
	// String contextPath = request.getContextPath();
	// // String path = requestGlobals.getRequest().getContextPath();
	//
	// String modulePath = null;
	// if (moduleBaseURL != null) {
	// try {
	// modulePath = new URL(moduleBaseURL).getPath();
	// } catch (MalformedURLException ex) {
	// // log the information, we will default
	// logger.info("Malformed moduleBaseURL: {} {}", moduleBaseURL, ex);
	// }
	// }
	//
	// SerializationPolicy serializationPolicy = null;
	//
	// /*
	// * Check that the module path must be in the same web app as the servlet
	// * itself. If you need to implement a scheme different than this,
	// * override this method.
	// */
	// if (modulePath == null || !modulePath.startsWith(contextPath)) {
	// String message = "ERROR: The module path requested, "
	// + modulePath
	// + ", is not in the same web application as this servlet, "
	// + contextPath
	// +
	// ".  Your module may not be properly configured or your client and server code maybe out of date.";
	// logger.info(message);
	// } else {
	// // Strip off the context path from the module base URL. It should be
	// // a
	// // strict prefix.
	// String contextRelativePath = modulePath.substring(contextPath
	// .length());
	//
	// String serializationPolicyFilePath = SerializationPolicyLoader
	// .getSerializationPolicyFileName(contextRelativePath
	// + strongName);
	//
	// Resource resource = null;
	// InputStream is = null;
	// try {
	// resource = assetResourceLocator
	// .findClasspathResourceForPath(serializationPolicyFilePath);
	// is = resource.openStream();
	// } catch (IOException e1) {
	// logger.error("Resource lookup failed {} {}",
	// serializationPolicyFilePath, e1);
	// }
	//
	// try {
	// if (is != null) {
	// try {
	// serializationPolicy = SerializationPolicyLoader
	// .loadFromStream(is, null);
	// } catch (ParseException e) {
	// logger.info(
	// "ERROR: Failed to parse the policy file '{}' {}",
	// serializationPolicyFilePath, e);
	// } catch (IOException e) {
	// logger.info(
	// "ERROR: Could not read the policy file '{}' {}",
	// serializationPolicyFilePath, e);
	// }
	// } else {
	// logger.info(
	// "ERROR: The serialization policy file '{}' was not found; did you forget to include it in this deployment?",
	// serializationPolicyFilePath);
	// }
	// } finally {
	// if (is != null) {
	// try {
	// is.close();
	// } catch (IOException e) {
	// // Ignore this error
	// }
	// }
	// }
	// }
	//
	// return serializationPolicy;
	// }

	/**
	 * A buffered output stream that
	 */
	public class GwtOutputStream extends ServletOutputStream {

		private final HttpServletResponse response;
		private final HttpServletRequest request;

		private ByteArrayOutputStream byteArrayOutputStream;

		/**
		 * Initially the ByteArrayOutputStream, later the response output stream
		 * (possibly wrapped with a GZIPOutputStream).
		 */
		private OutputStream currentOutputStream;

		public GwtOutputStream(HttpServletRequest request,
				HttpServletResponse response) {
			this.request = request;
			this.response = response;

			byteArrayOutputStream = new ByteArrayOutputStream();
			currentOutputStream = byteArrayOutputStream;
		}

		private void openResponseOutputStream() throws IOException {

			OutputStream responseOutputStream = response.getOutputStream();

			currentOutputStream = new BufferedOutputStream(responseOutputStream);

			try {
				String payload = byteArrayOutputStream.toString("UTF-8");
				System.out
						.println("GWTServletHttpRequestFilter.GwtOutputStream.openResponseOutputStream() "
								+ "Payload = " + payload);

				if (payload.startsWith("//")) {
					// It's already encoded
					currentOutputStream.write(payload.getBytes());
				} else if (payload.startsWith("/")) {
					Method method = ((RPCRequest) request
							.getAttribute(RPCRequest.class.getName()))
							.getMethod();
					String encoded = RPC.encodeResponseForSuccess(method,
							payload);
					currentOutputStream.write(encoded.getBytes());
				} else {
					try {
						// Convert to Tapestry JSONObject
						JSONObject jsonObject = new JSONObject(payload);
						if (jsonObject.has("content")) {
							payload = jsonObject.getString("content");
						} else if (jsonObject.has("redirectURL")) {
							payload = jsonObject.getString("redirectURL");
						}
						Method method = ((RPCRequest) request
								.getAttribute(RPCRequest.class.getName()))
								.getMethod();
						String encoded = RPC.encodeResponseForSuccess(method,
								payload);
						// Write what content we already have to the new stream.

						currentOutputStream.write(encoded.getBytes());
					} catch (Throwable e) {
						Method method = ((RPCRequest) request
								.getAttribute(RPCRequest.class.getName()))
								.getMethod();
						String encoded = RPC
								.encodeResponseForFailure(method, e);
						currentOutputStream.write(encoded.getBytes());
					}
				}

			} catch (SerializationException e) {
				e.printStackTrace();
				throw new RuntimeException(e);
			}

		}

		public void write(int b) throws IOException {
			currentOutputStream.write(b);
		}

		@Override
		public void write(byte[] b) throws IOException {
			currentOutputStream.write(b);
		}

		@Override
		public void write(byte[] b, int off, int len) throws IOException {
			currentOutputStream.write(b, off, len);
		}

		@Override
		public void flush() throws IOException {
			forceOutputStream().flush();
		}

		@Override
		public void close() throws IOException {
			// When closing, if we haven't accumulated enough output yet to
			// send what we have

			forceOutputStream().close();
		}

		private OutputStream forceOutputStream() throws IOException {
			if (byteArrayOutputStream != null) {
				openResponseOutputStream();
			}

			return currentOutputStream;
		}
	}

}
